Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 84617fee19f9f4dc0ee8ef84a5022ac093d79854


Parents : 78e8ba4
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-07T05:45:39-05:00

feat(NomadNetwork): update context menu actions with error handling and user feedback through ToastUtils

Changes
Diff

diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkBrowser.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkBrowser.vue
index b68fcaeb..26ee5cb6 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkBrowser.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkBrowser.vue
@@ -98,6 +98,7 @@ import GlobalState from "../../js/GlobalState";
import GlobalEmitter from "../../js/GlobalEmitter";
import { loadNomadTabs, saveNomadTabs } from "../../js/browserLayoutStore";
import LinkUtils from "../../js/LinkUtils";
+import ToastUtils from "../../js/ToastUtils";
export default {
name: "NomadNetworkBrowser",
@@ -605,21 +606,33 @@ export default {
closeContextMenu() {
this.contextMenu.show = false;
},
+ async runContextPageAction(actionFn) {
+ const page = this.contextPageRef;
+ if (!page) {
+ ToastUtils.warning(this.$t("nomadnet.context_menu_page_unavailable"));
+ this.closeContextMenu();
+ return;
+ }
+ try {
+ await actionFn(page);
+ } catch (error) {
+ console.error("nomad browser context menu action failed", error);
+ ToastUtils.error(this.$t("nomadnet.context_menu_action_failed"));
+ } finally {
+ this.closeContextMenu();
+ }
+ },
onContextViewSource() {
- this.contextPageRef?.showPageSource?.();
- this.closeContextMenu();
+ this.runContextPageAction((page) => page.showPageSource());
},
- async onContextReload() {
- await this.contextPageRef?.reloadNodePage?.();
- this.closeContextMenu();
+ onContextReload() {
+ this.runContextPageAction((page) => page.reloadNodePage());
},
- async onContextFavorite() {
- await this.contextPageRef?.toggleFavouriteFromContext?.();
- this.closeContextMenu();
+ onContextFavorite() {
+ this.runContextPageAction((page) => page.toggleFavouriteFromContext());
},
- async onContextDownloadPage() {
- await this.contextPageRef?.downloadPageToDisk?.();
- this.closeContextMenu();
+ onContextDownloadPage() {
+ this.runContextPageAction((page) => page.downloadPageToDisk());
},
onContextCloseTabsRight() {
const tabId = this.contextMenu.tabId ?? this.activeTabId;

diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index 9c4718a1..1920df70 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -1520,30 +1520,28 @@ export default {
);
},
async addFavourite(node) {
- // add to favourites
try {
await window.api.post("/api/v1/favourites/add", {
destination_hash: node.destination_hash,
display_name: node.display_name,
aspect: "nomadnetwork.node",
});
+ await this.getFavourites();
+ return true;
} catch (e) {
console.log(e);
+ return false;
}
-
- // update favourites
- this.getFavourites();
},
async removeFavourite(node) {
- // remove from favourites
try {
await window.api.delete(`/api/v1/favourites/${node.destination_hash}`);
+ await this.getFavourites();
+ return true;
} catch (e) {
console.log(e);
+ return false;
}
-
- // update favourites
- this.getFavourites();
},
async onBulkRemoveFavourites(hashes) {
if (!Array.isArray(hashes) || hashes.length === 0) {
@@ -1987,32 +1985,49 @@ export default {
},
showPageSource() {
if (!this.nodePagePath) {
- return;
+ ToastUtils.warning(this.$t("nomadnet.view_source_unavailable"));
+ return false;
}
this.isShowingNodePageSource = true;
+ return true;
},
async toggleFavouriteFromContext() {
if (!this.selectedNode?.destination_hash) {
- return;
+ ToastUtils.warning(this.$t("nomadnet.context_menu_page_unavailable"));
+ return false;
}
- if (this.isFavourite(this.selectedNode.destination_hash)) {
- await this.removeFavourite(this.selectedNode);
- return;
+ const wasFavourite = this.isFavourite(this.selectedNode.destination_hash);
+ const ok = wasFavourite
+ ? await this.removeFavourite(this.selectedNode)
+ : await this.addFavourite(this.selectedNode);
+ if (!ok) {
+ ToastUtils.error(this.$t("nomadnet.context_menu_favourite_failed"));
+ return false;
}
- await this.addFavourite(this.selectedNode);
+ ToastUtils.success(
+ wasFavourite ? this.$t("nomadnet.favourite_removed") : this.$t("nomadnet.favourite_added")
+ );
+ return true;
},
async downloadPageToDisk() {
if (!this.nodePageContent || !this.nodePagePath || this.isFailedPageContent(this.nodePageContent)) {
ToastUtils.warning(this.$t("nomadnet.download_page_unavailable"));
- return;
+ return false;
}
const parsed = this.parseNomadnetworkUrl(this.nodePagePath);
const pathPart = parsed?.pagePath || this.nodePagePath;
const segments = String(pathPart).split("/").filter(Boolean);
const filename = segments.length > 0 ? segments[segments.length - 1] : "nomad-page.txt";
- const blob = new Blob([this.nodePageContent], { type: "text/plain;charset=utf-8" });
- await DownloadUtils.downloadFile(filename, blob);
- ToastUtils.success(this.$t("nomadnet.download_page_started"));
+ try {
+ const blob = new Blob([this.nodePageContent], { type: "text/plain;charset=utf-8" });
+ await DownloadUtils.downloadFile(filename, blob);
+ ToastUtils.success(this.$t("nomadnet.download_page_started"));
+ return true;
+ } catch (error) {
+ console.error("nomad page download failed", error);
+ ToastUtils.error(this.$t("nomadnet.download_page_failed"));
+ return false;
+ }
},
onPageContextMenu(event) {
if (this.embedded && this.nomadBrowserTabActions) {
@@ -2035,21 +2050,27 @@ export default {
closeStandaloneContextMenu() {
this.standaloneContextMenu.show = false;
},
+ async runStandaloneContextAction(actionFn) {
+ try {
+ await actionFn();
+ } catch (error) {
+ console.error("nomad page context menu action failed", error);
+ ToastUtils.error(this.$t("nomadnet.context_menu_action_failed"));
+ } finally {
+ this.closeStandaloneContextMenu();
+ }
+ },
onStandaloneContextViewSource() {
- this.showPageSource();
- this.closeStandaloneContextMenu();
+ this.runStandaloneContextAction(() => this.showPageSource());
},
- async onStandaloneContextReload() {
- await this.reloadNodePage();
- this.closeStandaloneContextMenu();
+ onStandaloneContextReload() {
+ this.runStandaloneContextAction(() => this.reloadNodePage());
},
- async onStandaloneContextFavorite() {
- await this.toggleFavouriteFromContext();
- this.closeStandaloneContextMenu();
+ onStandaloneContextFavorite() {
+ this.runStandaloneContextAction(() => this.toggleFavouriteFromContext());
},
- async onStandaloneContextDownloadPage() {
- await this.downloadPageToDisk();
- this.closeStandaloneContextMenu();
+ onStandaloneContextDownloadPage() {
+ this.runStandaloneContextAction(() => this.downloadPageToDisk());
},
async reloadNodePage() {
// reload current node page without adding to history and without using cache

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 9d3354de..e2606cae 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -1625,7 +1625,21 @@
"bulk_add_to_favourites": "Zu Favoriten hinzufügen",
"bulk_add_favourites_done": "{count} Knoten zu Favoriten hinzugefügt",
"bulk_block_nodes": "Verbannen",
- "bulk_nothing_to_add_favourites": "Nichts hinzuzufügen (bereits Favoriten oder nichts ausgewählt)"
+ "bulk_nothing_to_add_favourites": "Nichts hinzuzufügen (bereits Favoriten oder nichts ausgewählt)",
+ "context_tabs": "Tabs",
+ "close_tabs_to_right": "Tabs rechts schließen",
+ "close_other_tabs": "Andere Tabs schließen",
+ "close_all_tabs": "Alle Tabs schließen",
+ "download_page": "Seite herunterladen",
+ "download_page_started": "Seiten-Download gestartet",
+ "download_page_unavailable": "Kein Seiteninhalt zum Herunterladen verfügbar",
+ "download_page_failed": "Seite konnte nicht heruntergeladen werden",
+ "view_source_unavailable": "Keine Seite geladen, um Quelltext anzuzeigen",
+ "context_menu_page_unavailable": "Keine aktive Seite für diesen Tab",
+ "context_menu_action_failed": "Kontextmenü-Aktion fehlgeschlagen",
+ "context_menu_favourite_failed": "Favorit konnte nicht aktualisiert werden",
+ "favourite_added": "Zu Favoriten hinzugefügt",
+ "favourite_removed": "Aus Favoriten entfernt"
},
"forwarder": {
"title": "LXMF-Weiterleiter",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 3af39f07..9dbdd95c 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1744,7 +1744,14 @@
"close_all_tabs": "Close all tabs",
"download_page": "Download page",
"download_page_started": "Page download started",
- "download_page_unavailable": "No page content available to download"
+ "download_page_unavailable": "No page content available to download",
+ "download_page_failed": "Could not download page",
+ "view_source_unavailable": "No page loaded to view source",
+ "context_menu_page_unavailable": "No active page for this tab",
+ "context_menu_action_failed": "Context menu action failed",
+ "context_menu_favourite_failed": "Could not update favourite",
+ "favourite_added": "Added to favourites",
+ "favourite_removed": "Removed from favourites"
},
"forwarder": {
"title": "LXMF Forwarder",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index af9ba899..a5a4ec5c 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1737,7 +1737,21 @@
"bulk_add_to_favourites": "Añadir a favoritos",
"bulk_add_favourites_done": "Se añadieron {count} nodos a favoritos",
"bulk_block_nodes": "Desterrar",
- "bulk_nothing_to_add_favourites": "Nada que añadir (ya son favoritos o no hay selección)"
+ "bulk_nothing_to_add_favourites": "Nada que añadir (ya son favoritos o no hay selección)",
+ "context_tabs": "Pestañas",
+ "close_tabs_to_right": "Cerrar pestañas a la derecha",
+ "close_other_tabs": "Cerrar otras pestañas",
+ "close_all_tabs": "Cerrar todas las pestañas",
+ "download_page": "Descargar página",
+ "download_page_started": "Descarga de página iniciada",
+ "download_page_unavailable": "No hay contenido de página disponible para descargar",
+ "download_page_failed": "No se pudo descargar la página",
+ "view_source_unavailable": "No hay página cargada para ver el código fuente",
+ "context_menu_page_unavailable": "No hay página activa para esta pestaña",
+ "context_menu_action_failed": "Error en la acción del menú contextual",
+ "context_menu_favourite_failed": "No se pudo actualizar el favorito",
+ "favourite_added": "Añadido a favoritos",
+ "favourite_removed": "Eliminado de favoritos"
},
"forwarder": {
"title": "LXMF Forwarder",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index d103183d..207b50b9 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -1737,7 +1737,21 @@
"bulk_add_to_favourites": "Lisää suosikiksi",
"bulk_add_favourites_done": "Lisättiin {count} solmua suosikkeihin",
"bulk_block_nodes": "Karkota",
- "bulk_nothing_to_add_favourites": "Ei lisättävää (solmut jo suosikeissa tai mitään ei ole valittu)"
+ "bulk_nothing_to_add_favourites": "Ei lisättävää (solmut jo suosikeissa tai mitään ei ole valittu)",
+ "context_tabs": "Välilehdet",
+ "close_tabs_to_right": "Sulje välilehdet oikealta",
+ "close_other_tabs": "Sulje muut välilehdet",
+ "close_all_tabs": "Sulje kaikki välilehdet",
+ "download_page": "Lataa sivu",
+ "download_page_started": "Sivun lataus aloitettu",
+ "download_page_unavailable": "Ei sivun sisältöä ladattavaksi",
+ "download_page_failed": "Sivua ei voitu ladata",
+ "view_source_unavailable": "Ei ladattua sivua lähteen katsomiseen",
+ "context_menu_page_unavailable": "Ei aktiivista sivua tälle välilehdelle",
+ "context_menu_action_failed": "Kontekstivalikon toiminto epäonnistui",
+ "context_menu_favourite_failed": "Suosikkia ei voitu päivittää",
+ "favourite_added": "Lisätty suosikkeihin",
+ "favourite_removed": "Poistettu suosikeista"
},
"forwarder": {
"title": "LXMF-välittäjä",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 582596f2..98fd1b90 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1737,7 +1737,21 @@
"bulk_add_to_favourites": "Ajouter aux favoris",
"bulk_add_favourites_done": "{count} nœuds ajoutés aux favoris",
"bulk_block_nodes": "Bannir",
- "bulk_nothing_to_add_favourites": "Rien à ajouter (déjà favoris ou aucune sélection)"
+ "bulk_nothing_to_add_favourites": "Rien à ajouter (déjà favoris ou aucune sélection)",
+ "context_tabs": "Onglets",
+ "close_tabs_to_right": "Fermer les onglets à droite",
+ "close_other_tabs": "Fermer les autres onglets",
+ "close_all_tabs": "Fermer tous les onglets",
+ "download_page": "Télécharger la page",
+ "download_page_started": "Téléchargement de la page démarré",
+ "download_page_unavailable": "Aucun contenu de page disponible à télécharger",
+ "download_page_failed": "Impossible de télécharger la page",
+ "view_source_unavailable": "Aucune page chargée pour afficher la source",
+ "context_menu_page_unavailable": "Aucune page active pour cet onglet",
+ "context_menu_action_failed": "Échec de l'action du menu contextuel",
+ "context_menu_favourite_failed": "Impossible de mettre à jour le favori",
+ "favourite_added": "Ajouté aux favoris",
+ "favourite_removed": "Retiré des favoris"
},
"forwarder": {
"title": "Expéditeur LXMF",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index ac29c0e1..1c3a2c55 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -1789,7 +1789,21 @@
"bulk_add_to_favourites": "Aggiungi ai preferiti",
"bulk_add_favourites_done": "Aggiunti {count} nodi ai preferiti",
"bulk_block_nodes": "Bandisci",
- "bulk_nothing_to_add_favourites": "Niente da aggiungere (già preferiti o nessuna selezione)"
+ "bulk_nothing_to_add_favourites": "Niente da aggiungere (già preferiti o nessuna selezione)",
+ "context_tabs": "Schede",
+ "close_tabs_to_right": "Chiudi schede a destra",
+ "close_other_tabs": "Chiudi altre schede",
+ "close_all_tabs": "Chiudi tutte le schede",
+ "download_page": "Scarica pagina",
+ "download_page_started": "Download della pagina avviato",
+ "download_page_unavailable": "Nessun contenuto di pagina disponibile per il download",
+ "download_page_failed": "Impossibile scaricare la pagina",
+ "view_source_unavailable": "Nessuna pagina caricata per visualizzare il sorgente",
+ "context_menu_page_unavailable": "Nessuna pagina attiva per questa scheda",
+ "context_menu_action_failed": "Azione del menu contestuale non riuscita",
+ "context_menu_favourite_failed": "Impossibile aggiornare il preferito",
+ "favourite_added": "Aggiunto ai preferiti",
+ "favourite_removed": "Rimosso dai preferiti"
},
"forwarder": {
"title": "Inoltro LXMF",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index bd1c1a99..198cec4c 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1737,7 +1737,21 @@
"bulk_add_to_favourites": "Toevoegen aan favorieten",
"bulk_add_favourites_done": "{count} nodes toegevoegd aan favorieten",
"bulk_block_nodes": "Verbannen",
- "bulk_nothing_to_add_favourites": "Niets toe te voegen (al favoriet of geen selectie)"
+ "bulk_nothing_to_add_favourites": "Niets toe te voegen (al favoriet of geen selectie)",
+ "context_tabs": "Tabbladen",
+ "close_tabs_to_right": "Tabbladen rechts sluiten",
+ "close_other_tabs": "Andere tabbladen sluiten",
+ "close_all_tabs": "Alle tabbladen sluiten",
+ "download_page": "Pagina downloaden",
+ "download_page_started": "Paginadownload gestart",
+ "download_page_unavailable": "Geen pagina-inhoud beschikbaar om te downloaden",
+ "download_page_failed": "Pagina kon niet worden gedownload",
+ "view_source_unavailable": "Geen pagina geladen om broncode te bekijken",
+ "context_menu_page_unavailable": "Geen actieve pagina voor dit tabblad",
+ "context_menu_action_failed": "Contextmenu-actie mislukt",
+ "context_menu_favourite_failed": "Favoriet kon niet worden bijgewerkt",
+ "favourite_added": "Toegevoegd aan favorieten",
+ "favourite_removed": "Verwijderd uit favorieten"
},
"forwarder": {
"title": "LXMF Forwarder",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 20d27cb1..621456b5 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -1625,7 +1625,21 @@
"bulk_add_to_favourites": "В избранное",
"bulk_add_favourites_done": "Добавлено узлов в избранное: {count}",
"bulk_block_nodes": "Изгнать",
- "bulk_nothing_to_add_favourites": "Нечего добавлять (уже в избранном или нет выбора)"
+ "bulk_nothing_to_add_favourites": "Нечего добавлять (уже в избранном или нет выбора)",
+ "context_tabs": "Вкладки",
+ "close_tabs_to_right": "Закрыть вкладки справа",
+ "close_other_tabs": "Закрыть другие вкладки",
+ "close_all_tabs": "Закрыть все вкладки",
+ "download_page": "Скачать страницу",
+ "download_page_started": "Загрузка страницы начата",
+ "download_page_unavailable": "Нет содержимого страницы для загрузки",
+ "download_page_failed": "Не удалось скачать страницу",
+ "view_source_unavailable": "Нет загруженной страницы для просмотра исходника",
+ "context_menu_page_unavailable": "Нет активной страницы для этой вкладки",
+ "context_menu_action_failed": "Не удалось выполнить действие контекстного меню",
+ "context_menu_favourite_failed": "Не удалось обновить избранное",
+ "favourite_added": "Добавлено в избранное",
+ "favourite_removed": "Удалено из избранного"
},
"forwarder": {
"title": "LXMF Форвардер",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index d7e3114a..83ee1a1c 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1737,7 +1737,21 @@
"bulk_add_to_favourites": "加入收藏",
"bulk_add_favourites_done": "已将 {count} 个节点加入收藏",
"bulk_block_nodes": "移出",
- "bulk_nothing_to_add_favourites": "无需添加(已在收藏或未选择)"
+ "bulk_nothing_to_add_favourites": "无需添加(已在收藏或未选择)",
+ "context_tabs": "标签页",
+ "close_tabs_to_right": "关闭右侧标签页",
+ "close_other_tabs": "关闭其他标签页",
+ "close_all_tabs": "关闭所有标签页",
+ "download_page": "下载页面",
+ "download_page_started": "已开始下载页面",
+ "download_page_unavailable": "没有可下载的页面内容",
+ "download_page_failed": "无法下载页面",
+ "view_source_unavailable": "未加载页面,无法查看源代码",
+ "context_menu_page_unavailable": "此标签页没有活动页面",
+ "context_menu_action_failed": "上下文菜单操作失败",
+ "context_menu_favourite_failed": "无法更新收藏",
+ "favourite_added": "已添加到收藏",
+ "favourite_removed": "已从收藏中移除"
},
"forwarder": {
"title": "LXMF 转发器",

diff --git a/tests/frontend/NomadNetworkBrowser.test.js b/tests/frontend/NomadNetworkBrowser.test.js
index ba14551c..67cdb658 100644
--- a/tests/frontend/NomadNetworkBrowser.test.js
+++ b/tests/frontend/NomadNetworkBrowser.test.js
@@ -16,6 +16,16 @@ vi.mock("@/components/nomadnetwork/NomadNetworkPage.vue", () => ({
}));
import NomadNetworkBrowser from "@/components/nomadnetwork/NomadNetworkBrowser.vue";
+import ToastUtils from "@/js/ToastUtils";
+
+vi.mock("@/js/ToastUtils", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ warning: vi.fn(),
+ info: vi.fn(),
+ },
+}));
const MaterialDesignIconStub = {
name: "MaterialDesignIcon",
@@ -46,6 +56,7 @@ describe("NomadNetworkBrowser.vue", () => {
beforeEach(() => {
localStorage.clear();
routerReplace = undefined;
+ vi.clearAllMocks();
});
it("creates a single tab on mount", () => {
@@ -364,4 +375,49 @@ describe("NomadNetworkBrowser.vue", () => {
expect(wrapper.vm.tabs).toHaveLength(1);
expect(wrapper.vm.tabs[0].destinationHash).toBe("");
});
+
+ describe("context menu", () => {
+ it("openTabContextMenu selects tab and shows menu", () => {
+ const wrapper = mountBrowser();
+ wrapper.vm.addTab("a".repeat(32), null, "Tab A");
+ const tab = wrapper.vm.tabs[1];
+ wrapper.vm.openTabContextMenu({ clientX: 12, clientY: 34 }, tab);
+ expect(wrapper.vm.activeTabId).toBe(tab.id);
+ expect(wrapper.vm.contextMenu.show).toBe(true);
+ expect(wrapper.vm.contextMenu.tabId).toBe(tab.id);
+ expect(wrapper.vm.contextMenu.x).toBe(12);
+ expect(wrapper.vm.contextMenu.y).toBe(34);
+ });
+
+ it("runContextPageAction warns when page ref is missing", async () => {
+ const wrapper = mountBrowser();
+ wrapper.vm.contextMenu.show = true;
+ await wrapper.vm.runContextPageAction(() => Promise.resolve());
+ expect(ToastUtils.warning).toHaveBeenCalledWith("nomadnet.context_menu_page_unavailable");
+ expect(wrapper.vm.contextMenu.show).toBe(false);
+ });
+
+ it("runContextPageAction surfaces unexpected errors", async () => {
+ const wrapper = mountBrowser();
+ const tabId = wrapper.vm.tabs[0].id;
+ wrapper.vm.pageRefs[tabId] = {
+ reloadNodePage: vi.fn().mockRejectedValue(new Error("boom")),
+ };
+ wrapper.vm.contextMenu.tabId = tabId;
+ await wrapper.vm.runContextPageAction((page) => page.reloadNodePage());
+ expect(ToastUtils.error).toHaveBeenCalledWith("nomadnet.context_menu_action_failed");
+ expect(wrapper.vm.contextMenu.show).toBe(false);
+ });
+
+ it("onContextFavorite delegates to page toggleFavouriteFromContext", async () => {
+ const wrapper = mountBrowser();
+ const tabId = wrapper.vm.tabs[0].id;
+ const toggleFavouriteFromContext = vi.fn().mockResolvedValue(true);
+ wrapper.vm.pageRefs[tabId] = { toggleFavouriteFromContext };
+ wrapper.vm.contextMenu.tabId = tabId;
+ await wrapper.vm.onContextFavorite();
+ expect(toggleFavouriteFromContext).toHaveBeenCalledOnce();
+ expect(wrapper.vm.contextMenu.show).toBe(false);
+ });
+ });
});

diff --git a/tests/frontend/NomadNetworkPage.test.js b/tests/frontend/NomadNetworkPage.test.js
index 86a45481..a3785d05 100644
--- a/tests/frontend/NomadNetworkPage.test.js
+++ b/tests/frontend/NomadNetworkPage.test.js
@@ -11,6 +11,16 @@ vi.mock("vuetify/components/VTooltip", () => ({
}));
import NomadNetworkPage from "@/components/nomadnetwork/NomadNetworkPage.vue";
+import ToastUtils from "@/js/ToastUtils";
+
+vi.mock("@/js/ToastUtils", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ warning: vi.fn(),
+ info: vi.fn(),
+ },
+}));
vi.mock("@/js/WebSocketConnection", () => ({
default: {
@@ -30,6 +40,7 @@ describe("NomadNetworkPage.vue", () => {
delete: vi.fn(),
};
window.api = axiosMock;
+ vi.clearAllMocks();
axiosMock.get.mockImplementation((url) => {
if (url === "/api/v1/favourites") return Promise.resolve({ data: { favourites: [] } });
@@ -511,10 +522,16 @@ describe("NomadNetworkPage.vue", () => {
wrapper.vm.selectedNode = { destination_hash: "a".repeat(32), display_name: "Node" };
wrapper.vm.nodePagePath = `${"a".repeat(32)}:/page/index.mu`;
wrapper.vm.isShowingNodePageSource = false;
- wrapper.vm.showPageSource();
+ expect(wrapper.vm.showPageSource()).toBe(true);
expect(wrapper.vm.isShowingNodePageSource).toBe(true);
});
+ it("showPageSource warns when no page path is loaded", () => {
+ const wrapper = mountNomadNetworkPage();
+ expect(wrapper.vm.showPageSource()).toBe(false);
+ expect(ToastUtils.warning).toHaveBeenCalledWith("nomadnet.view_source_unavailable");
+ });
+
it("downloadPageToDisk saves current page content", async () => {
const DownloadUtils = (await import("@/js/DownloadUtils")).default;
const downloadFile = vi.spyOn(DownloadUtils, "downloadFile").mockResolvedValue(undefined);
@@ -522,9 +539,73 @@ describe("NomadNetworkPage.vue", () => {
wrapper.vm.selectedNode = { destination_hash: "a".repeat(32), display_name: "Node" };
wrapper.vm.nodePagePath = `${"a".repeat(32)}:/page/index.mu`;
wrapper.vm.nodePageContent = "Hello Nomad";
- await wrapper.vm.downloadPageToDisk();
+ expect(await wrapper.vm.downloadPageToDisk()).toBe(true);
expect(downloadFile).toHaveBeenCalledWith("index.mu", expect.any(Blob));
+ expect(ToastUtils.success).toHaveBeenCalledWith("nomadnet.download_page_started");
+ downloadFile.mockRestore();
+ });
+
+ it("downloadPageToDisk warns when page content is unavailable", async () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.nodePagePath = `${"a".repeat(32)}:/page/index.mu`;
+ wrapper.vm.nodePageContent = null;
+ expect(await wrapper.vm.downloadPageToDisk()).toBe(false);
+ expect(ToastUtils.warning).toHaveBeenCalledWith("nomadnet.download_page_unavailable");
+ });
+
+ it("downloadPageToDisk reports download failures", async () => {
+ const DownloadUtils = (await import("@/js/DownloadUtils")).default;
+ const downloadFile = vi.spyOn(DownloadUtils, "downloadFile").mockRejectedValue(new Error("disk full"));
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.nodePagePath = `${"a".repeat(32)}:/page/index.mu`;
+ wrapper.vm.nodePageContent = "Hello Nomad";
+ expect(await wrapper.vm.downloadPageToDisk()).toBe(false);
+ expect(ToastUtils.error).toHaveBeenCalledWith("nomadnet.download_page_failed");
downloadFile.mockRestore();
});
+
+ it("toggleFavouriteFromContext adds favourite and toasts success", async () => {
+ axiosMock.post.mockResolvedValueOnce({ data: {} });
+ axiosMock.get.mockResolvedValueOnce({ data: { favourites: [] } });
+ const wrapper = mountNomadNetworkPage();
+ const node = { destination_hash: "a".repeat(32), display_name: "Node" };
+ wrapper.vm.selectedNode = node;
+ wrapper.vm.favourites = [];
+ expect(await wrapper.vm.toggleFavouriteFromContext()).toBe(true);
+ expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/favourites/add", {
+ destination_hash: node.destination_hash,
+ display_name: node.display_name,
+ aspect: "nomadnetwork.node",
+ });
+ expect(ToastUtils.success).toHaveBeenCalledWith("nomadnet.favourite_added");
+ });
+
+ it("toggleFavouriteFromContext reports API failures", async () => {
+ axiosMock.post.mockRejectedValueOnce(new Error("network"));
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.selectedNode = { destination_hash: "a".repeat(32), display_name: "Node" };
+ wrapper.vm.favourites = [];
+ expect(await wrapper.vm.toggleFavouriteFromContext()).toBe(false);
+ expect(ToastUtils.error).toHaveBeenCalledWith("nomadnet.context_menu_favourite_failed");
+ });
+
+ it("onPageContextMenu delegates to browser tab actions when embedded", () => {
+ const openContextMenu = vi.fn();
+ const wrapper = mountNomadNetworkPage({ destinationHash: "", embedded: true });
+ wrapper.vm.nomadBrowserTabActions = { openContextMenu };
+ const event = { clientX: 4, clientY: 8 };
+ wrapper.vm.onPageContextMenu(event);
+ expect(openContextMenu).toHaveBeenCalledWith(event);
+ });
+
+ it("runStandaloneContextAction closes menu after failures", async () => {
+ const wrapper = mountNomadNetworkPage();
+ wrapper.vm.standaloneContextMenu.show = true;
+ await wrapper.vm.runStandaloneContextAction(() => {
+ throw new Error("boom");
+ });
+ expect(ToastUtils.error).toHaveBeenCalledWith("nomadnet.context_menu_action_failed");
+ expect(wrapper.vm.standaloneContextMenu.show).toBe(false);
+ });
});
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────